// Go作者版权所有。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

package buildid

import (
	"bytes"
	"debug/elf"
	"fmt"
	"internal/xcoff"
	"io"
	"io/fs"
	"os"
	"strconv"
	"strings"
)

var (
	errBuildIDMalformed = fmt.Errorf("malformed object file")

	bangArch = []byte("!<arch>")
	pkgdef   = []byte("__.PKGDEF")
	goobject = []byte("go object ")
	buildid  = []byte("build id ")
)

// ReadFile从存档文件或可执行文件中读取生成ID。
func ReadFile(name string) (id string, err error) {
	f, err := os.Open(name)
	if err != nil {
		return "", err
	}
	defer f.Close()

	buf := make([]byte, 8)
	if _, err := f.ReadAt(buf, 0); err != nil {
		return "", err
	}
	if string(buf) != "!<arch>\n" {
		if string(buf) == "<bigaf>\n" {
			return readGccgoBigArchive(name, f)
		}
		return readBinary(name, f)
	}

	// 读取足够的目标以获取构建ID。
	// 归档文件的外观应如下所示：
	// None
	// !<拱门>
	// __.PKGDEF 0 0 644 7955`
	// go对象darwin amd64开发X:无
	// 构建id“B41E5C4525E25C9FD5E9F9A1DE7857EA0D41224”
	// None
	// 可变大小的字符串是GOOS、GOARCH和实验列表（X:none）。
	// 读取前1024个字节应该足够了。
	data := make([]byte, 1024)
	n, err := io.ReadFull(f, data)
	if err != nil && n == 0 {
		return "", err
	}

	tryGccgo := func() (string, error) {
		return readGccgoArchive(name, f)
	}

	// 存档标题。
	for i := 0; ; i++ { // 在i==3期间返回
		j := bytes.IndexByte(data, '\n')
		if j < 0 {
			return tryGccgo()
		}
		line := data[:j]
		data = data[j+1:]
		switch i {
		case 0:
			if !bytes.Equal(line, bangArch) {
				return tryGccgo()
			}
		case 1:
			if !bytes.HasPrefix(line, pkgdef) {
				return tryGccgo()
			}
		case 2:
			if !bytes.HasPrefix(line, goobject) {
				return tryGccgo()
			}
		case 3:
			if !bytes.HasPrefix(line, buildid) {
				// 找到了对象头，只是没有生成id行。
				// 视为成功，生成id为空。
				return "", nil
			}
			id, err := strconv.Unquote(string(line[len(buildid):]))
			if err != nil {
				return tryGccgo()
			}
			return id, nil
		}
	}
}

// readGccgoArchive尝试将归档解析为标准Unix
// 归档文件，并从_buildid.o条目中获取构建ID。
// _buildid.o条目由（*Builder）.gccgoBuildIDELFFile编写
// 在cmd/go/internal/work/exec.go中。
func readGccgoArchive(name string, f *os.File) (string, error) {
	bad := func() (string, error) {
		return "", &fs.PathError{Op: "parse", Path: name, Err: errBuildIDMalformed}
	}

	off := int64(8)
	for {
		if _, err := f.Seek(off, io.SeekStart); err != nil {
			return "", err
		}

		// TODO（iant）：制作一个调试/ar包，并使用它
		// 在这里和cmd/link中。
		var hdr [60]byte
		if _, err := io.ReadFull(f, hdr[:]); err != nil {
			if err == io.EOF {
				// 没有更多条目，没有生成ID。
				return "", nil
			}
			return "", err
		}
		off += 60

		sizeStr := strings.TrimSpace(string(hdr[48:58]))
		size, err := strconv.ParseInt(sizeStr, 0, 64)
		if err != nil {
			return bad()
		}

		name := strings.TrimSpace(string(hdr[:16]))
		if name == "_buildid.o/" {
			sr := io.NewSectionReader(f, off, size)
			e, err := elf.NewFile(sr)
			if err != nil {
				return bad()
			}
			s := e.Section(".go.buildid")
			if s == nil {
				return bad()
			}
			data, err := s.Data()
			if err != nil {
				return bad()
			}
			return string(data), nil
		}

		off += size
		if off&1 != 0 {
			off++
		}
	}
}

// readGccgoBigArchive尝试将归档解析为AIX大文件
// 归档文件，并从_buildid.o条目中获取构建ID。
// _buildid.o条目由（*Builder）.gccgobuildidxcoffile编写
// 在cmd/go/internal/work/exec.go中。
func readGccgoBigArchive(name string, f *os.File) (string, error) {
	bad := func() (string, error) {
		return "", &fs.PathError{Op: "parse", Path: name, Err: errBuildIDMalformed}
	}

	// 读取固定长度标题。
	if _, err := f.Seek(0, io.SeekStart); err != nil {
		return "", err
	}
	var flhdr [128]byte
	if _, err := io.ReadFull(f, flhdr[:]); err != nil {
		return "", err
	}
	// 读取第一个成员偏移量。
	offStr := strings.TrimSpace(string(flhdr[68:88]))
	off, err := strconv.ParseInt(offStr, 10, 64)
	if err != nil {
		return bad()
	}
	for {
		if off == 0 {
			// 没有更多条目，没有生成ID。
			return "", nil
		}
		if _, err := f.Seek(off, io.SeekStart); err != nil {
			return "", err
		}
		// 读取成员标题。
		var hdr [112]byte
		if _, err := io.ReadFull(f, hdr[:]); err != nil {
			return "", err
		}
		// 读取成员名称长度。
		namLenStr := strings.TrimSpace(string(hdr[108:112]))
		namLen, err := strconv.ParseInt(namLenStr, 10, 32)
		if err != nil {
			return bad()
		}
		if namLen == 10 {
			var nam [10]byte
			if _, err := io.ReadFull(f, nam[:]); err != nil {
				return "", err
			}
			if string(nam[:]) == "_buildid.o" {
				sizeStr := strings.TrimSpace(string(hdr[0:20]))
				size, err := strconv.ParseInt(sizeStr, 10, 64)
				if err != nil {
					return bad()
				}
				off += int64(len(hdr)) + namLen + 2
				if off&1 != 0 {
					off++
				}
				sr := io.NewSectionReader(f, off, size)
				x, err := xcoff.NewFile(sr)
				if err != nil {
					return bad()
				}
				data := x.CSect(".go.buildid")
				if data == nil {
					return bad()
				}
				return string(data), nil
			}
		}

		// 读取下一个成员偏移量。
		offStr = strings.TrimSpace(string(hdr[20:40]))
		off, err = strconv.ParseInt(offStr, 10, 64)
		if err != nil {
			return bad()
		}
	}
}

var (
	goBuildPrefix = []byte("\xff Go build ID: \"")
	goBuildEnd    = []byte("\"\n \xff")

	elfPrefix = []byte("\x7fELF")

	machoPrefixes = [][]byte{
		{0xfe, 0xed, 0xfa, 0xce},
		{0xfe, 0xed, 0xfa, 0xcf},
		{0xce, 0xfa, 0xed, 0xfe},
		{0xcf, 0xfa, 0xed, 0xfe},
	}
)

var readSize = 32 * 1024 // 更改为测试

// readBinary从二进制文件读取生成ID。
// None
// ELF二进制文件将构建ID存储在适当的PT_注释部分中。
// None
// 其他二进制格式没有那么灵活。对于这些，链接器
// 在最开始时将生成ID存储为非指令字节
// 文本段的名称，该名称应显示在开头附近
// 文件的一部分。这是笨拙的，但相当便携。自定义位置
// 可以根据需要为其他二进制类型添加，就像我们为ELF所做的那样。
func readBinary(name string, f *os.File) (id string, err error) {
	// 读取二进制文件的前32KB。
	// 这应该足以找到构建ID。
	// 在ELF文件中，构建ID位于前导标头中，
	// 通常小于4KB，更不用说32KB了。
	// 在Mach-O文件中，没有限制，因此我们必须解析该文件。
	// 在其他系统上，我们正在努力阅读足够的内容
	// 我们在阅读中得到文本段的开头。
	// 文本段在hello中开始的偏移量
	// 当今世界为每种不同的对象格式编译：
	// None
	// 计划9:0x20
	// Windows:0x600
	// None
	data := make([]byte, readSize)
	_, err = io.ReadFull(f, data)
	if err == io.ErrUnexpectedEOF {
		err = nil
	}
	if err != nil {
		return "", err
	}

	if bytes.HasPrefix(data, elfPrefix) {
		return readELF(name, f, data)
	}
	for _, m := range machoPrefixes {
		if bytes.HasPrefix(data, m) {
			return readMacho(name, f, data)
		}
	}
	return readRaw(name, data)
}

// readRaw查找存储在文本段数据中的原始生成ID。
func readRaw(name string, data []byte) (id string, err error) {
	i := bytes.Index(data, goBuildPrefix)
	if i < 0 {
		// 丢失的视为成功，但生成ID为空。
		return "", nil
	}

	j := bytes.Index(data[i+len(goBuildPrefix):], goBuildEnd)
	if j < 0 {
		return "", &fs.PathError{Op: "parse", Path: name, Err: errBuildIDMalformed}
	}

	quoted := data[i+len(goBuildPrefix)-1 : i+len(goBuildPrefix)+j+1]
	id, err = strconv.Unquote(string(quoted))
	if err != nil {
		return "", &fs.PathError{Op: "parse", Path: name, Err: errBuildIDMalformed}
	}
	return id, nil
}

// HashToString将哈希h转换为要记录的字符串
// 包内存档和二进制文件作为构建ID的一部分。
// 我们使用散列的前120位（5个块，每个块24位）并编码
// 它在base64中运行，产生一个20字节的字符串。因为这只用于
// 检测是否需要重建已安装的文件（不用于查找
// 在对象文件缓存中），120位足以驱动
// 错误“不需要重建”决策的概率实际上为零。
// 我们在档案中嵌入了两个不同的哈希，在二进制文件中嵌入了四个，
// 因此，当显示构建ID时，减少到20字节是一个显著的节约。
// （20*4+3=83个字节，而
// 更简单的选项是在base64中打印整个h）。
func HashToString(h [32]byte) string {
	const b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
	const chunks = 5
	var dst [chunks * 4]byte
	for i := 0; i < chunks; i++ {
		v := uint32(h[3*i])<<16 | uint32(h[3*i+1])<<8 | uint32(h[3*i+2])
		dst[4*i+0] = b64[(v>>18)&0x3F]
		dst[4*i+1] = b64[(v>>12)&0x3F]
		dst[4*i+2] = b64[(v>>6)&0x3F]
		dst[4*i+3] = b64[v&0x3F]
	}
	return string(dst[:])
}
